<template>
    <div class="talkContent">
        <div class="talkShow">
            <div>
                <div :class="[(item.person === 'mechanical') ? 'mechanicalTalk' : 'mineTalk']" v-for="(item,index) in talkList" :key="index">
                    <div class="icon" style="text-align: center; font-size: 14px">
                        <Icon :type="item.person === 'mechanical' ? 'logo-octocat' : 'md-person'" />
                    </div>
                    <div class="content">
                        <div v-if="item.person === 'mechanical'">
                            <div v-html="item.say" id="liContainer"></div>
                            <div v-if="item.img" v-html="item.img"></div>
                        </div>
                        <div v-else>{{ item.say }}</div>
                        <div v-if="computeHasSource(item)">
                          <span style="font-weight: 600; color: #8190B2;">相关来源</span>
                          <Collapse v-model="item.activeNames">
                            <Panel v-for="(v, index) in item.relevantSource" :key="v.docId" :name="v.docId">
                                {{ index + 1 }}、{{ v.title }}
                                <template #content>
                                    <span v-html="v.summary" />
                                </template>
                            </Panel>
                          </Collapse>
                        </div>
                        <!-- 由AI生成仅供参考字段 -->
                        <div v-if="item.label" style="font-weight: 600; color: #A0A0A0;">
                          {{ item.label }}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import { getToken } from './api'
import { Message } from "view-design";
import crypto from 'crypto';

const initMachineSay = `<div style="font-weight: 600;">您好，我是乌鲁木齐智能问答。</div>
                            <div>您可以像这样对我提问：</div>
                            <ul style="padding-left: 0.2rem; margin: 0.2rem 0;">
                                <li class="exampleItem">消防泵房作用及组成</li>
                                <li class="exampleItem">空压机作用</li>
                                <li class="exampleItem">干燥塔工作原理</li>
                                <li class="exampleItem">给水功能间作用及组成</li>
                            </ul>
                          <div>我将为您提供乌鲁木齐智能相关知识解答。</div>`

export default {
    data() {
        return {
            talkList: [
                {
                    id:"1",
                    person: 'mechanical',
                    say: initMachineSay,
                },
            ],
            token: null,
            timestamp: '',
            sign: '', // 签名
            dialoguaContent: '', // 对话内容集合：isDialoguaStart为true的情况下，item的集合
            hasImg: false,
            imgUrl: '',
            isImgStart: false,
            count: 0,
            publicUrl: 'https://gjgwllm.pipechina.com.cn/gateway/yunding-manage/document/downloadImage',
            questionStr: '',
            // isrepeat: false
        };
    },
    mounted() {
        this.$nextTick(() => {
            this.setEventForLi('addEventListener'); // 为事例问题添加点击事件
            window.addEventListener('message', this.receiveMessage, false) // 接收录音设备传递过来的问题
        });

    },
    beforeDestroy() {
        window.removeEventListener('message', this.receiveMessage, false)
        this.setEventForLi('removeEventListener');
    },
    methods: {
        receiveMessage(e) {
            // if (e.origin === 'http://127.0.0.1:8001'  && e.data.type === 'question') {
            //     console.log(e.origin) // ⽗⻚⾯所在的域
            //     console.log(e.data)  // ⽗⻚⾯发送的消息
            // }

            // if(this.isrepeat) {
            //     return
            // }

            // if(e.origin === 'http://localhost:8083') {
            //     this.isrepeat = true
            //     console.log('接收的数据', e);
                this.questionStr = e?.data?.message || '空压机工作原理?'; // 通过语音录入提出的问题
                this.getQuestion(false, this.questionStr);
            // }
        },

        // 获取答案
        getQuestion(isExampleIssue = false, exampleIssue) {
            if(isExampleIssue) {
                this.questionStr = exampleIssue
            }
            if (this.questionStr === '') {
                Message.error('请输入内容');
                return;
            }
            this.talkList.push({ id: Date.now(), person: 'admin', say: this.questionStr });
            this.setT()
        },

        // 给li标签设置事件
        setEventForLi(type) {
            const container = document.getElementById('liContainer');
            if (container) {
                const elements = container.querySelectorAll('li');
                elements.forEach(el => {
                    el[type]('click', () => {
                        const title = el.textContent;
                        this.getQuestion(true, title);
                        // 点击的瞬间，li标签由hover时的红色字体变为蓝色，改善闪动效果
                        el.classList.add('active');
                        setTimeout(() => {
                            el.classList.remove('active');
                        }, 800)
                    });
                });
            }
        },
        // 获取秘钥---用于获取token
        setT () {
            const appKey = "hegsChatKey";
            const userCode = "T1041059";
            const timestamp = Date.now();
            const clientSecret = "c4896e40eb3b4b719bf780a4d2187243";
            const attachment = appKey + userCode + timestamp;
            // 生成 HMAC-SHA256 签名
            const hmac = crypto.createHmac('sha256', clientSecret);
            hmac.update(attachment);
            const signMsg = hmac.digest('hex');
            this.sign = signMsg;
            this.timestamp = timestamp;
            this.handlerGetData();
        },
        // 处理数据
        async handlerGetData () {
            this.token = await this.getUserToken();
            // 请求体
            const data = {
                scene: '11',
                question: this.questionStr,
                deviceSource: '0' // 设备来源：0-PC端 1-移动端
            }
            // 请求头
            const headers = {
                'Content-Type': 'application/json',
                'Accept': 'text/event-stream',
                'Authorization': 'Bearer ' + this.token
            }
            this.talkList.push({id: Date.now(), person: 'mechanical', say: '思考中...', relevantSource: [], label: ''});
            // 调用获取答案接口
            fetch('https://gjgwllm.pipechina.com.cn/gateway/yunding-qserver/stream/chat/answer', {
                method: 'post',
                headers,
                body: JSON.stringify(data)
            }).then(async res => {
                console.log('接口返回数据', res);
                this.imgUrl = ''
                if (res.status === 401) {
                    this.setT()
                    this.talkList = [
                        {
                            id: "1",
                            person: 'mechanical',
                            say: initMachineSay
                        },
                    ]
                    Message.warning({
                        content: '您的token已过期，正在为您重置token, 请稍后',
                        duration: 3
                    })
                }
                this.talkList[this.talkList.length - 1].label = '由AI生成仅供参考';
                this.talkList[this.talkList.length - 1].relevantSource = [];
                if (res.status === 400) {
                    this.talkList[this.talkList.length - 1].say = '抱歉，您的这个问题可能涉及敏感信息，我暂时不清楚应该如何回复，请注意规范使用。'
                } else {
                    this.talkList[this.talkList.length - 1].say = ''; // 将内容置空
                    // 读取二进制流
                    const reader = res.body.getReader()
                    // 将读取的二进制流数据解析成  utf-8的字符串
                    const textDecoder = new TextDecoder('utf-8')
                    let result = true
                    let isDialoguaStart = false

                    // 读取数据的逻辑：
                    // 1、还没读取完，解码读取的数据，转为数组，过滤出数组中有效的数据
                    // 2、循环遍历数组：
                    //     如果item中有dialogueId，将item拼接到this.dialoguaContent里，当前循环结束，进入下一个循环，后面的循环也都是将item拼接到this.dialoguaContent里
                    //     如果item中没有dialogueId，最终走appendSubText方法，将item.choices[0].message.content内容拼接到最后一个pop的say属性中
                    // 3、如果数据读取完了，处理dialoguaContent中的相关来源数据，获取到最后一个pop的相关来源数据

                    // 循环读取数据
                    while (result) {
                        const {done, value} = await reader.read();
                        if (done) {
                            reader.releaseLock(); // 释放读取器锁 防止内存泄露
                            this.hasImg = false
                            this.isImgStart = false
                            isDialoguaStart = false
                            result = false;
                            const dialoguaContent = JSON.parse(this.dialoguaContent)
                            console.log('数据读取完后的对话内容', dialoguaContent);

                            if (dialoguaContent.answerSourceList && dialoguaContent.answerSourceList.length > 0) {
                                dialoguaContent.answerSourceList.forEach(item => {
                                    try {
                                        let pattern = /\{img(.*?)}/g
                                        const repl = item.summary.match(pattern)[0] // 检索字符串中与正则表达式相匹配的结果，返回数组，没找到返回null
                                        item.summary = item.summary.replace(repl, `<img src="${this.publicUrl}?imgId=${repl.slice(1, -1)}"/>`)
                                    } catch (_) {
                                        console.error('转换失败')
                                    }
                                })
                            }
                            console.log('done后的操作', dialoguaContent)
                            await this.appendText(dialoguaContent)
                            this.$nextTick(() => {
                                this.dialoguaContent = ''
                            })
                            break;
                        }
                        // 将读取的数据进行解码
                        const text = textDecoder.decode(value, {stream: true})
                        console.log('解码读取到的数据', text);

                        let _data = text?.split(/\n\n/) || []
                        console.log('_data数组', _data);


                        let data = _data.filter(item => item !== 'data:[DONE]' && item !== 'data:[CONTENT_DONE]' && item)
                        console.log('11data数组', data);

                        for (const it of data) {
                            // 每次返回的数据都是 data: 开头的 将其替换为空--只将 data: 替换为 ''
                            let item = it.replace(/^(data:)/g, '');
                            console.log('data数组中的每一项', item);

                            // 是否是相关来源字段标识 dialogueId
                            if (item.includes('{"dialogueId":')) isDialoguaStart = true // 只要有一个item里面包含dialogueId，它后面的数据都会走到if (isDialoguaStart)判断里面
                            console.log('是否有对话id', isDialoguaStart);

                            if (isDialoguaStart) {
                                this.dialoguaContent += item
                                console.log('对话内容', this.dialoguaContent);
                                continue // 跳出当前循环，进入下一个循环
                            }
                            let res = JSON.parse(item)
                            console.log('转换后的item', res);

                            await this.appendText(res.choices[0].delta.content) // 如果不包含 dialogueId 的数据发送给了 父级页面
                            // 将回答内容发给父页面---最后一个参数url待完善
                            window.top.postMessage({
                                type: 'answer',
                                content: res.choices[0].delta.content || ''
                            }, '*');
                        }
                    }
                }
            })
        },
        // 获取token
        async getUserToken () {
            const params = {
                appKey: 'hegsChatKey',
                userCode: 'T1041059',
                timestamp: this.timestamp,
                sign: this.sign,
                clientSecret: 'c4896e40eb3b4b719bf780a4d2187243'
            };
            const { data: { access_token } } = await getToken(params);
            if (access_token) return await Promise.resolve(access_token);
        },
        // 处理解析出来的元素进行页面展示
        async appendText (text) {
            console.log('处理过的元素', text, text.hasOwnProperty('dialogueId'));

            if (text.hasOwnProperty('dialogueId')) {
                this.talkList[this.talkList.length - 1].say && (this.talkList[this.talkList.length - 1].say += '<br/>'); // 有值的时候再换行，没内容不要换行

                // 换行符
                const reg = /^\d+\.|\。|\n|(^\d+\.\d+$)|(^\(\d+\\)$ /g
                text.answerSourceList.forEach(item => {
                    item.summary = item.summary.split(reg).map(v => {
                        if (v) return `<br/>${v}` // 这边加了 br title和summary内容的距离会比较大
                    }).filter(v => v).join('。')
                });
                this.talkList[this.talkList.length - 1].relevantSource = text?.answerSourceList || []
                console.log('最后pop的来源', this.talkList[this.talkList.length - 1].relevantSource);

            } else {
                this.appendSubText(text)
            }
        },
        appendSubText (text) {
            // data循环的时候：
            // 1、第一条数据为'{',isImgStart置为true
            // 2、第二条数据不为'{'，将第二条数据拼接到imgUrl上，count累加变为1。this.hasImg为false，this.isImgStart为true，直接return
            // 3、第三条数据不为'{'，将第二条数据拼接到imgUrl上，count累加变为2。this.hasImg为false，this.isImgStart为true，直接return
            // 4、第四条数据不为'{'，将第二条数据拼接到imgUrl上，count累加变为3。this.hasImg为false，this.isImgStart为true，直接return
            // 5、第五条数据不为'{'，将第二条数据拼接到imgUrl上，count累加变为4。将count置为0，isImgStart置为false。
            //     判断拼接后的imgUrl是否===img-
            //         如果等于，hasImg置为true，处理最后一条pop的img展示
            //         如果不等于，hasImg置为false，将imgUrl拼接到say上，将第五条数据text拼接到say上。如果数据中包含换行，加br


            // isImgStart 控制的是：是否是图片的开始  图片开始时数据 是以 { 开始
            if (text === '{') {
                this.isImgStart = true
                return
            }

            // 如果是图片  将每次的串进行累加
            if (this.isImgStart) {
                this.imgUrl += text
                this.count++
            }
            // count累计到4如果结果是 img- 标识是图片
            if (this.count === 4) {
                this.count = 0
                this.isImgStart = false
                // 对非图片情况做判断
                if (this.imgUrl !== 'img-') {
                    this.hasImg = false
                    this.talkList[this.talkList.length - 1].say += this.imgUrl;
                    this.imgUrl = ''
                } else {
                    // 是图片使用hasImg进行标识  进行下一步
                    this.hasImg = true
                }
            }
            // 如果这时候还不确定是不是图片  那么就先进行累加展示
            if (!this.hasImg) {
                if (this.isImgStart) return
                this.talkList[this.talkList.length - 1].say += text;
                console.log('最后一个pop', this.talkList[this.talkList.length - 1].say);

                if (text && text.includes('\n')) this.talkList[this.talkList.length - 1].say += '<br/>'
                if (text && text.includes('。')) this.talkList[this.talkList.length - 1].say += '<br/>'
            } else {
                // 最后的 } 不要拼接到文本上
                if (text === '}') return
                if (this.imgUrl === 'img--') this.imgUrl = 'img-'
                this.talkList[this.talkList.length - 1].img = `<img src='${this.publicUrl}?imgId=${this.imgUrl += text}' />`
            }
        },

        // 判断是否有相关来源
        computeHasSource(object) {
            return object.hasOwnProperty('relevantSource') && object.relevantSource.length > 0
        },
        // 滚动到最底部
        scrollToBottom() {
            this.$nextTick(()=> {
                let box = this.$el.querySelector(".talkShow");
                box.scrollTop = box.scrollHeight;
            })
        },
    },
    watch: {},
    created() {},
    updated() {
        this.scrollToBottom();
    },
};
</script>
<style lang="scss">
.ivu-layout-content {
    background: #01103B !important;
    padding: 0 !important;
}
.exampleItem {
    list-style: none;
    color: #276AFF;
    cursor: pointer;
    &:hover {
        color: red;;
    }
    &.active {
        color: red;
    }
}
</style>
<style scoped lang="scss">
.scrollable-element {
    width: 300px;
    height: 200px;
    overflow-y: auto;
    /* 内容有溢出时出现滚动条 */
    background-color: #f0f0f0;
}

.scrollable-element::-webkit-scrollbar {
    display: none;
}
.talkContent {
    width: 100% !important;
    height: 98vh !important;
    padding: 0 16px;
    font-size: .6rem;
    font-family: PingFangSC-Medium;
}

.talkShow {
    width: 100%;
    height: calc(100% - 56px);
    overflow-y: auto;
    border-radius: 10px;
}
.talkShow::-webkit-scrollbar {
    display: none;
}

.mechanicalTalk {
    margin: 10px 0;
    display: flex;
    align-items: flex-start;
}

.mechanicalTalk {
    .icon {
        display: inline-block;
        width: 25px;
        height: 25px;
        border-radius: 5px;
        background-color: #fff;
        margin-right: 10px;
        line-height: 25px;
    }
    .content {
        padding: 5px 8px;
        background-color: white;
        border-radius: 10px;
        word-break: break-all;
        text-align: left;
        max-width: calc(100% - 35px);
        span {
            width: 100%;
        }
    }
}

.mineTalk {
    display: flex;
    justify-content: flex-end;
    align-items: flex-start;
}

.mineTalk {
    .icon {
        display: inline-block;
        width: 25px;
        height: 25px;
        border-radius: 5px;
        background-color: #fff;
        line-height: 25px;
        order: 1;
    }
    .content {
        border-radius: 10px;
        background: #409eff;
        color: #fff;
        padding: 5px 8px;
        word-break: break-all;
        text-align: left;
        margin-right: 10px;
        order: 0;
        span {
            background-color: transparent;
            width: 100%;
        }
    }
}
::v-deep .ivu-collapse>.ivu-collapse-item>.ivu-collapse-header {
    line-height: normal;
    height: fit-content;
}
::v-deep .ivu-collapse {
    background-color: #F8F9FA;
    border-radius: 8px;
    margin-bottom: 10px;
    border: none;
    .ivu-collapse-item {
        border: none;
        .ivu-collapse-header {
            color: #276AFF !important;
            border: none !important;
            word-break: keep-all;
            padding-left: .2rem;
            .ivu-icon {
                display: none;
            }
            &:hover {
                color: red !important;
            }
        }
        .el-collapse-item__arrow {
            display: none;
        }
    }
}
::v-deep .ivu-input {
    height: 36px !important;
}
</style>

